<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * JSON_RPC_Server
 *
 * @package JSON_RPC
 * @author Matt
 * @version SVN: $Id$
 */


class JSON_RPC_Server	{
	// JSON-RPC version (see : http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html)
	const JSON_RPC_VERSION = '1.1';
	// Service description version (see : http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html#ServiceDescription)
	const SERVICE_DESCRIPTION_VERSION = '1.0';   
	
	//const JSON_RPC_CONTENT_TYPE = 'application/json';
	const JSON_RPC_CONTENT_TYPE = 'text/plain';
	
	public $methods = array();

	protected $service 	= null;
	protected $obj 		= null;
	protected $req_id	= null;
	protected $reserved_methods 	= array('system.describe' => 'describeService');
	protected $_service_properties 	= array('name' 	=> 'REQUIRED', 
																					'id'	=> 'REQUIRED', 
																					'version'=>'OPTIONAL',
																					'summary'=>'OPTIONAL',
																					'help'	=> 'OPTIONAL',
																					'address'=>'OPTIONAL',
																					'procs'	=>'OPTIONAL'
																					);
	protected $socket_mode = false;
	protected $socket_addr = '';
	
	/**
		* Constructor
		*
		* @param object $obj Class instance that we want to make available through JSON-RPC
		* @param array $renamed_methods Map of methods we want to rename
		* @access public
	***/
	public function __construct( $obj = null, $renamed_methods = array(), $socket_mode = false, $socket_addr = '' ) {
		$this->socket_mode = $socket_mode;
		$this->socket_addr = $socket_addr;
		if(!is_null($obj)) {
			$this->obj = $obj;
			foreach($renamed_methods as $internal_name => $external_name) {
				$this->methods[$external_name] = $internal_name;
			}
		}
	}
	 
	/**
		* Declare Service
		*
		* @param array $serv_props
		* @access public
	***/
	public function declareService( $serv_props = array() ) {
		// init $this->service to a simple stdClass
		$this->service = new stdClass();
		foreach ($this->_service_properties as $key => $val) {
			if( $val == 'REQUIRED' && ( !array_key_exists($key, $serv_props) || empty($serv_props[$key]) ) ) {
				$this->error('InvalidServiceDescription', 503, 'Invalid service description');
			}
			if(array_key_exists($key, $serv_props)) {
				$this->service->$key = $serv_props[$key];
			}
		}
	}
	
	/**
		* Register a simple function
		*
		* @param string $internal_name internal function name
		* @param string $external_name external function name, if different
		* @access public
	***/
	public function registerFunction($internal_name, $external_name = '' ) {
		$external_name = empty($external_name) ? $internal_name : $external_name;
		$this->methods[$external_name] = $internal_name;    	
	}
	
	/**
		* Start listening
		*
		* @access public
		* @todo Implementation of socket transport handling
	***/
	public function run() {
		// the server run in socket mode
		if($this->socket_mode == true) {
			// the server run in HTTP mode	
		}
		else {
			// the server handles a HTTP POST request
			if($_SERVER['REQUEST_METHOD'] == 'POST') {
				$json_data = file_get_contents('php://input');
				$data = json_decode($json_data, true);
				if(!$data) {
					$this->error("InvalidJSONRPCRequest", 501, "Invalid JSON request. JSON cannot be parsed. JSON was : ".$json_data);
				}
			}
			// the server handles a HTTP GET request
			elseif ($_SERVER['REQUEST_METHOD'] == 'GET') {
				// if magic quotes active, strip antislashes from data
				if(get_magic_quotes_gpc() == 1) {
					$_GET = array_map('stripslashes', $_GET);
				}
				// handle null id
				if($_GET['id'] == 'null' || !array_key_exists('id', $_GET)) {
					$_GET['id'] = null;
				}
				// we use Positional Parameters
				if(!isset($_GET['kwparams'])) {
					$data = array('method' => @$_GET['method'], 'id' => @$_GET['id'] );
					unset($_GET['method']);
					unset($_GET['id']);
					$data['params'] = $_GET;
					// we use named parameters
				}
				else	{
					
				}
			}
			
			$this->req_id = $data['id'];
			define('JSONRPC_REQUEST_ID', $this->req_id);
			$this->callMethod($data['method'], $data['params']);
		}
	}
	
	/**
		* Return Service Description
		*
		* @access protected
		* @return json object
	***/
	protected function getServiceDescription() {
		return json_encode($this->service);
	}
	
	/**
		* Call method / function
		*
		* @param string $method
		* @param array $args
		* @access protected 
	***/
	protected function callMethod($method, $args) {
		if(array_key_exists($method, $this->methods)) {
			$method = $this->methods[$method];
		}
		
		if( !is_null($this->obj) && method_exists($this->obj, $method)) {
			$call = @call_user_func_array(array($this->obj, $method), $args);
		} 
		elseif (function_exists($method)) {
			$call = @call_user_func_array($method, $args);
		} 
		else	{
			return $this->error("UndefinedMethod", 502, "Method/function '$method' does not exist.");
		}
		
		// function/method call worked, output result
		$this->output($call);
	}
	
	/**
		* Output JSON-RPC response
		*
		* @param object $data result data
		* @param object $error error data
		* @access protected 
	***/
	protected function output($data = null, $error = null) {
		$response = new stdClass();
		$response->version = JSON_RPC_Server::JSON_RPC_VERSION;
		// there is some data to outupt
		if(!is_null($data)) {
			$response->result = $data;
		}
		
		// there was an error
		if(!is_null($error)) {
			$response->error = $error;
		}
		else	{
			$response->error = null;
		}
		header('Content-Type: '.JSON_RPC_Server::JSON_RPC_CONTENT_TYPE);
		
		// final output
		if(defined('JSONRPC_REQUEST_ID')) {
			$response->id = JSONRPC_REQUEST_ID;
		} 
		else {
			$response->id = $response->req_id;
		}
		echo json_encode($response);
	}
	
	/**
		* Throw an error
		*
		* @param string $err_name Error name
		* @param integer $err_code Error code
		* @param string $err_msg Error message
		* @access protected 
	***/
	public function error($err_name, $err_code, $err_msg) {
		$error 			= new stdClass();
		$error->name 	= $err_name;
		$error->code 	= $err_code;
		$error->message = $err_msg;
		$this->output(null, $error);
	}
}

// global error handling
function JSONRPC_ErrorHandler($errno, $errstr, $errfile, $errline)	{
	$json_server = new JSON_RPC_Server();
	
	switch ($errno) {
		case E_USER_ERROR:
			$json_server->error('JSONRPC_PHPError', 504, "$errstr");
			exit(1);
			break;
		case E_USER_WARNING:
		case E_USER_NOTICE:
		case E_NOTICE:
		case E_USER_NOTICE:
		case E_STRICT:
		break;
		default:
			$json_server->error('JSONRPC_PHPError', 505, "$errstr");
			exit(1);
			break;
	}
	
	return true;
}

set_error_handler('JSONRPC_ErrorHandler');
/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * c-hanging-comment-ender-p: nil
 * End:
 */
?>